# -*- coding: utf-8 -*-
""" A Degrees Widget module to handles different representations

      * Degrees.decimals,
      * Degrees Minutes.decimals,
      * Degrees, Minutes, Seconds.decimals

    :Author:
      - 20111202-20120214 Roberto Vidmar

    :Revision:  $Revision: 52 $
                $Date: 2012-11-12 09:07:15 +0000 (Mon, 12 Nov 2012) $

    :Copyright: 2011-2012
                Nicola Creati <ncreati@inogs.it>
                Roberto Vidmar <rvidmar@inogs.it>

    :License: MIT/X11 License (see :download:`license.txt
                               <../../license.txt>`)
"""

from qtCompat import QtCore, Qt, Signal, QtGui

QCA = QtCore.QCoreApplication
from degrees import deg2dms, deg2dm, dms2deg

#===============================================================================
class DValidator(QtGui.QRegExpValidator):
  """ A validator for Degrees in formats defined by regular Expression
  """
  def __init__(self, parent, regexp):
    """ Create a new instance of the validator.

     .. note:: parent `AND` regexp must be supplied.

    :param parent: parent widget
    :type parent: QtGui widget
    :param regexp: regular expression that will be used for validation.
    :type regexp: QtCore.QRegExp
    :raises:
    """
    qregexp = QtCore.QRegExp(regexp)
    super(DValidator, self).__init__(qregexp, parent)

#===============================================================================
class AbstractDEditor(QtGui.QLineEdit):
  """ Abstract Line Editor class for degrees handling
  """
  def __init__(self, parent):
    """ Create a new instance of the editor.

     .. note:: This class should not be used without subclassing.

    :param parent: parent widget
    :type parent: QtGui widget
    :raises:
    """
    super(AbstractDEditor, self).__init__(self.defaultValue, parent)

    self.dWidget = self.parent()

    #font = QtGui.QFont("Monospace", 12, QtGui.QFont.Bold)
    font = QtGui.QFont("Courier New", 12, QtGui.QFont.Bold)
    font.setLetterSpacing(QtGui.QFont.AbsoluteSpacing, -0.5)
    self.setFont(font)

    # Set Validator
    validator = DValidator(self, self.regexp)
    self.setValidator(validator)
    # Set Input Mask (needed to activate overwrite mode)
    inputMask = ("x") * len(self.defaultValue)
    self.setInputMask(inputMask)

    # Set line editor width in number of characters
    fm = QtGui.QFontMetrics(self.font())
    width = fm.width(self.inputMask())
    self.setMinimumWidth(width)
    self.setMaximumWidth(width)
    self.setAlignment(self.dWidget.align)

  def signAndText(self):
    """ Return sign and text of this QLineEdit

    :returns: sign and text of this QLineEdit
    :rtype: tuple
    :raises:
    """
    t = self.text()
    if t[0] == self.dWidget.signschr[0]:
      return '+', t
    else:
      return '-', t

  def fvalue(self):
    """ Return floating point list of values

    :returns: floating point value
    :rtype: float
    :raises:
    """
    return [float(v) for v in self.value()]

  def svalue(self):
    """ Return widget text

    :returns: text content
    :rtype: string
    :raises:
    """
    return self.text()

  def setValue(self, value):
    """ Set Editor value

    :param value: value to set
    :type value: string, int or float
    :raises:
    """
    self.setText(self.asString(value))
#===============================================================================
class DEditor(AbstractDEditor):
  """ Degrees Line Editor

      Will contain float numbers (degrees) with the format **[+-]ddd.ddddddddd**
  """
  def __init__(self, parent):
    """ Create a new instance of the editor.

    :param parent: parent widget
    :type parent: QtGui widget
    :raises:
    """
    if parent.latitude:
      self.regexp = (u"[%s]([0-8][0-9]\.\d{9}[°]|90\.0{9}[°])" %
        parent.signschr)
    else:
      self.regexp = (u"[%s]((0\d\d|1[0-7]\d)\.\d{9}[°]|180.0{9}[°])" %
        parent.signschr)

    self.fmt = "%%s%%0%d.%df%s" % (parent.degreesLen + 1 + parent.ddecimals,
      parent.ddecimals, parent.degchr)

    self.defaultValue = self.fmt % (parent.signschr[0], 0.)
    super(DEditor, self).__init__(parent)

  def value(self):
    """ Return signed widget string value

    :returns: signed widget string value
    :rtype: string
    :raises:
    """
    sign, t = super(DEditor, self).signAndText()
    dd = str(t[1:-1])
    return (sign + dd, )

  def asString(self, value):
    """ Return value as formatted string

        Format is embedded into class.

    :param value: numerical value
    :type value: float or int
    :returns: `value` as formatted string
    :rtype: string
    :raises:
    """
    if value < 0:
      signchr = self.dWidget.signschr[1]
    else:
      signchr = self.dWidget.signschr[0]
    return self.fmt % (signchr, abs(value))

#===============================================================================
class DMEditor(AbstractDEditor):
  """ Degrees, Minutes Line Editor

      Will contain float numbers (degrees, minutes ) with the format
      **[+-]ddd mm.mmmmmm**
  """
  def __init__(self, parent):
    """ Create a new instance of the editor.

    :param parent: parent widget
    :type parent: QtGui widget
    :raises:
    """
    if parent.latitude:
      self.regexp = (u"[%s]([0-8][0-9][°][0-5]\d\.\d{6}'|90[°]00\.0{6}')" %
        parent.signschr)
    else:
      self.regexp = (u"[%s]((0\d\d|1[0-7]\d)[°][0-5]\d\.\d{4}'|"
        u"180[°]00\.0{6}')" % parent.signschr)

    self.fmt = "%%s%%0%dd%s%%0%d.%df%s" % (parent.degreesLen, parent.degchr,
      3 + parent.mdecimals, parent.mdecimals, parent.minchr)

    self.defaultValue = self.fmt % (parent.signschr[0], 0., 0.)
    super(DMEditor, self).__init__(parent)

  def value(self):
    """ Return signed widget string value

    :returns: signed widget string value
    :rtype: string
    :raises:
    """
    dl = self.dWidget.degreesLen
    sign, t = super(DMEditor, self).signAndText()
    dd = str(t[1: dl + 1])
    mm = str(t[dl + 2: -1])
    return sign + dd, mm

  def asString(self, value):
    """ Return value as formatted string

        Format is embedded into class.

    :param value: numerical value
    :type value: float or int
    :returns: `value` as formatted string
    :rtype: string
    :raises:
    """
    decimals = self.dWidget.mdecimals
    sign, degs, mins = deg2dm(value, decimals)
    if sign < 0:
      signchr = self.dWidget.signschr[1]
    else:
      signchr = self.dWidget.signschr[0]
    return self.fmt % (signchr, degs, mins)

#===============================================================================
class DMSEditor(AbstractDEditor):
  """ Degrees, Minutes, Seconds Line Editor

      Will contain float numbers (degrees, minutes ) with the format
      **[+-]ddd mm ss.ssss**
  """
  def __init__(self, parent):
    """ Create a new instance of the editor.

    :param parent: parent widget
    :type parent: QtGui widget
    :raises:
    """
    if parent.latitude:
      self.regexp = (u"[%s]([0-8][0-9][°][0-5]\d['][0-5]\d\.\d{4}\"|"
        u"90[°]00'00\.0{4}\")" % parent.signschr)
    else:
      self.regexp = (u"[%s]((0\d\d|1[0-7]\d)[°][0-5]\d['][0-5]\d\.\d{4}\"|"
        u"180[°]00'00\.0{4}\")" % parent.signschr)

    self.fmt = "%%s%%0%dd%s%%02d%s%%0%d.%df%s" % (
      parent.degreesLen, parent.degchr, parent.minchr,
      3 + parent.sdecimals, parent.sdecimals, parent.secchr)

    self.defaultValue = self.fmt % (parent.signschr[0], 0., 0., 0.)
    super(DMSEditor, self).__init__(parent)

  def value(self):
    """ Return signed widget string value

    :returns: signed widget string value
    :rtype: string
    :raises:
    """
    dl = self.dWidget.degreesLen
    sign, t = super(DMSEditor, self).signAndText()
    dd = str(t[1: dl + 1])
    mm = str(t[dl + 2: dl + 4])
    ss = str(t[dl + 5: -1])
    return sign + dd, mm, ss

  def asString(self, value):
    """ Return value as formatted string

        Format is embedded into class.

    :param value: numerical value
    :type value: float or int
    :returns: `value` as formatted string
    :rtype: string
    :raises:
    """
    decimals = self.dWidget.sdecimals
    sign, degs, mins, secs = deg2dms(value, decimals)
    if sign < 0:
      signchr = self.dWidget.signschr[1]
    else:
      signchr = self.dWidget.signschr[0]
    return self.fmt % (signchr, degs, mins, secs)

#===============================================================================
class DegreesWidget(QtGui.QWidget):
  """ A Degrees Widget that handles different representations:

        * Degrees.decimals,
        * Degrees Minutes.decimals,
        * Degrees, Minutes, Seconds.decimals
  """
  textEditedSignal = Signal(object)
  DDD = 0
  DMM = 1
  DMS = 2

  def __init__(self, *args, **kargs):
    """ Create a new instance of the widget.

        The first argument, if present, is the value to assign.

        The following keyword arguments are understood:

        - latitude  : widget will hold latitude [-90, 90] degrees.
                    Default is True
        - signstr   : characters identifying sign according to latitude.
                    Default is "NSEW"
        - degchr    : character for degrees.
                    Default is °
        - minchr    : character for minutes.
                    Default is '
        - secchr    : character for seconds.
                    Default is "
        - ddecimals : number of decimals for degrees representation.
                    Default is 9
        - mdecimals : number of decimals for degrees, minutes.
                    Default is 6
        - sdecimals : number of decimals for degrees, minutes, seconds.
                    Default is 4
        - rep       : representation to show after creation.
                    Default is :class:`DDD`
        - align     : alignment of the widget.
                    Default is Qt.AlignRight
        - readonly  : read only attribute.
                    Default is False
    """

    if len(args) > 1:
      value = args[1]
    else:
      value = None
    if len(args) > 0:
      parent = args[0]
    else:
      parent = None
    super(DegreesWidget, self).__init__(parent)

    self.latitude = kargs.pop('latitude', True)
    self.signstr = kargs.pop('signstr', u"NSEW")
    self.degchr = kargs.pop('degchr', u"\N{DEGREE SIGN}")
    self.minchr = kargs.pop('minchr', u"'")
    self.secchr = kargs.pop('secchr', u'"')
    self.ddecimals = kargs.pop('ddecimals', 9)
    self.mdecimals = kargs.pop('mdecimals', 6)
    self.sdecimals = kargs.pop('sdecimals', 4)
    rep = kargs.pop('rep', 0)
    self.align = kargs.pop('align', Qt.AlignRight)
    readonly = kargs.pop('readonly', False)

    if self.latitude:
      self.signschr = self.signstr[:2]
      self.degreesLen = 2
      self.setObjectName(QCA.translate("degreesWidget", "Latitude"))
    else:
      self.signschr = self.signstr[2:]
      self.degreesLen = 3
      self.setObjectName(QCA.translate("degreesWidget", "Longitude"))
    self.swidget = QtGui.QStackedWidget()
    for editor, toolTip in zip(
      (DEditor(self), DMEditor(self), DMSEditor(self)),
      (QCA.translate("degreesWidget",
      "Enter here %s in\ndegrees.decimals"),
      QCA.translate("degreesWidget",
      "Enter here %s in\ndegrees, minutes.decimals"),
      QCA.translate("degreesWidget",
      "Enter here %s in\ndegrees, minutes, seconds.decimals"))):
      editor.setToolTip(toolTip % str(self.objectName()).lower())
      self.swidget.addWidget(editor)
      editor.setReadOnly(readonly)
      editor.textEdited.connect(self.textEdited)

    # Representation
    self.setRep(rep)

    # Layout
    self._layout = QtGui.QVBoxLayout(self)
    self._layout.addWidget(self.swidget, alignment=Qt.AlignLeft|Qt.AlignVCenter)
    self._layout.setContentsMargins(0, 0, 0, 0)

    if value is not None:
      self.setValue(value)

  def textEdited(self, text):
    """ Update all widgets except the one that sent the signal

    :param text: the content of the editor widget
    :type text: string
    :raises:
    """
    # Update other widgets:
    for i in xrange(self.swidget.count()):
      widget = self.swidget.widget(i)
      if widget is not self.sender():
        widget.setValue(dms2deg(*self.sender().fvalue()))
    # Send signal with the new value
    self.textEditedSignal.emit(dms2deg(*self.sender().fvalue()))

  def setRep(self, rep=DDD):
    """ Set widget aspect (representation) according to rep:

    :param rep: new representation
    :type rep: any of :class:`DDD`, :class:`DMM`, :class:`DMS`
    :raises:
    """
    self.swidget.setCurrentIndex(rep)

  def fvalue(self, rep=None):
    """ Return widget float value(s) according to rep
        Default is current representation

    :param rep: representation
    :type rep: any of :class:`DDD`, :class:`DMM`, :class:`DMS`
    :returns: widget float value(s) according to rep
    :rtype: list
    :raises:
    """
    return [float(v) for v in self.tvalue(rep)]

  def tvalue(self, rep=None):
    """ Return widget tuple string value according to rep
        Default is current representation

    :param rep: representation
    :type rep: any of :class:`DDD`, :class:`DMM`, :class:`DMS`
    :returns: widget tuple string value according to rep
    :rtype: tuple
    :raises:
    """
    if rep is None:
      rep = self.swidget.currentIndex()

    # All editors have the same value
    return self.swidget.widget(rep).value()

  def svalue(self, rep=None):
    """ Return widget string value according to rep

        Default is current representation

    :param rep: representation
    :type rep: any of :class:`DDD`, :class:`DMM`, :class:`DMS`
    :returns: widget string value according to rep
    :rtype: tuple
    :raises:
    """
    if rep is None:
      rep = self.rep()

    return self.swidget.widget(rep).svalue()

  def setValue(self, *values):
    """ Set widget value.

      :param \*values: either degs *OR* (degs mins) *OR* (degs, mins, secs)
      :type \*values: float or tuple of floats
      :raises:
    """
    degrees = dms2deg(*values)

    # Set value to all widgets
    for i in xrange(self.swidget.count()):
      self.swidget.widget(i).setValue(degrees)

  def rep(self):
    """ Return current representation

    :returns: widget current representation
    :rtype: int
    :raises:
    """
    return self.swidget.currentIndex()

if __name__ == '__main__':
  import sys
  from signal import signal, SIGINT, SIG_DFL

  signal(SIGINT, SIG_DFL)
  app = QtGui.QApplication(sys.argv)
  w = DegreesWidget(None, latitude=False, rep=DegreesWidget.DMS)
  w.setValue(-123, 45, 54)
  w.show()
  sys.exit(app.exec_())
